//#define FAKE_BURNDOWN_UPDATE
namespace ScrumSprintMonitor.Implementation
{
	#region #using Directives

	using System;
	using System.Collections.Generic;
	using System.ComponentModel;
	using System.Diagnostics;
	using System.Linq;
	using System.Threading;

	using Infrastructure;

	#endregion

	public class SprintBurndown : ISprintBurndown,
	                              IDisposable
	{
		#region Private Fields

		private readonly BindingList<BurndownDataPoint> dataPointsList = new BindingList<BurndownDataPoint>();
		private readonly ITeamMemberFreeBusyInfo freeBusyInfo;
		private readonly ManualResetEvent processedBurndownDayDataEvent = new ManualResetEvent(true);
		private readonly ISprint sprint;
		private readonly SynchronizationContext syncContext;

		private readonly Dictionary<DateTime, IEnumerable<IWorkItemAdapter>> workItemHistoryDictionary =
			new Dictionary<DateTime, IEnumerable<IWorkItemAdapter>>();

		private bool disposed;

		private bool isBuildingDataPoints;
		private DateTime previousLocalBurndownUpdateTimeStamp = DateTime.MinValue;

		#endregion

		public SprintBurndown(ISprint sprint, ITeamMemberFreeBusyInfo freeBusyInfo)
		{
			if (freeBusyInfo == null)
			{
				throw new ArgumentNullException("freeBusyInfo");
			}

			this.sprint = sprint;
			this.freeBusyInfo = freeBusyInfo;

			this.syncContext = SynchronizationContext.Current;
		}

		~SprintBurndown()
		{
			Dispose(false);
		}

		#region Private Methods/Properties

		[DebuggerStepThrough]
		private void GuardDisposed()
		{
			if (this.disposed)
			{
				throw new ObjectDisposedException("Sprint");
			}
		}

		private void RetrieveDailyWorkItems()
		{
			if (IsBuildingDataPoints)
			{
				return;
			}

			IsBuildingDataPoints = true;

			try
			{
				DateTime at;

				for (at = this.sprint.Start; at <= SystemTime.Now() && at <= this.sprint.End; at = at.AddDays(1.0))
				{
					lock (this.sprint.TeamMembers)
					{
						var baseStationDate = at;
						if (this.sprint.TeamMembers.All(x => !this.freeBusyInfo.IsWorkDayFor(x.Information, baseStationDate)))
						{
							continue;
						}
					}

					lock (this.workItemHistoryDictionary)
					{
						bool validEntryAlreadyExists = this.workItemHistoryDictionary.ContainsKey(at) && at.Date < SystemTime.Now();
						if (validEntryAlreadyExists)
						{
							continue;
						}
					}

#if !FAKE_BURNDOWN_UPDATE
					var workItemsAtDate = this.sprint.WorkItemStore.GetWorkItemsAtDateWhere(at, w => w.Type == WorkItemType.SprintBacklogItem);
#endif

					this.processedBurndownDayDataEvent.WaitOne();
					this.processedBurndownDayDataEvent.Reset();

					lock (this.workItemHistoryDictionary)
					{
#if FAKE_BURNDOWN_UPDATE
						workItemHistoryDictionary[at] = null;
#else
						this.workItemHistoryDictionary[at] = workItemsAtDate.Where(w => w.Type == WorkItemType.SprintBacklogItem).ToList();
#endif

						this.syncContext.Post(state => ProcessIndividualAndTeamSprintBurndownForDay((DateTime) state), at);
					}
				}

				if (this.sprint.LastUpdate.HasValue && at >= SystemTime.Now() && at <= this.sprint.End)
				{
					ProcessSprintBurndownForLatestUpdate();
				}
			}
			finally
			{
				IsBuildingDataPoints = false;
			}
		}

		private void ProcessSprintBurndownForLatestUpdate()
		{
			if (!this.sprint.LastUpdate.HasValue)
			{
				return;
			}

			var slotToUpdate = this.previousLocalBurndownUpdateTimeStamp;

			this.syncContext.Post(state =>
			                      {
			                      	if (this.sprint.LastUpdate.HasValue)
			                      	{
			                      		this.processedBurndownDayDataEvent.WaitOne();
			                      		this.processedBurndownDayDataEvent.Reset();
			                      		ProcessIndividualAndTeamSprintBurndownForDay(this.sprint.LastUpdate.Value, slotToUpdate,
			                      		                                             this.sprint.WorkItemStore.WorkItems.Where(
			                      		                                             	w => w.Type == WorkItemType.SprintBacklogItem));
			                      	}
			                      }, null);

			this.previousLocalBurndownUpdateTimeStamp = this.sprint.LastUpdate.Value;
		}

		private void ProcessIndividualAndTeamSprintBurndownForDay(DateTime at)
		{
			ProcessSprintBurndownForDay(at);
			ProcessTeamMembersSprintBurndownForDay(at);

			this.processedBurndownDayDataEvent.Set();
		}

		private void ProcessIndividualAndTeamSprintBurndownForDay(DateTime at, DateTime slotToUpdate, IEnumerable<IWorkItemAdapter> workItems)
		{
			ProcessSprintBurndownForDay(workItems, at, slotToUpdate);
			ProcessTeamMembersSprintBurndownForDay(workItems, at, slotToUpdate);

			this.processedBurndownDayDataEvent.Set();
		}

		private void ProcessSprintBurndownForDay(DateTime at)
		{
			Debug.Assert(!Thread.CurrentThread.IsBackground);
			Debug.Assert(!this.processedBurndownDayDataEvent.WaitOne(0));

			lock (this.workItemHistoryDictionary)
			{
				var workItems = this.workItemHistoryDictionary[at];

				ProcessSprintBurndownForDay(workItems, at);
			}
		}

		private void ProcessSprintBurndownForDay(IEnumerable<IWorkItemAdapter> workItems, DateTime at)
		{
			ProcessSprintBurndownForDay(workItems, at, at);
		}

		private void ProcessSprintBurndownForDay(IEnumerable<IWorkItemAdapter> workItems, DateTime at, DateTime slotToUpdate)
		{
#if FAKE_BURNDOWN_UPDATE
			var random = 0;// new Random().Next(-100, 100);
			double remainingWork = (End - at).TotalHours + random;
			double doneWork = (at - Start).TotalHours + random;

			ChangeBurndownDataPoint(BurndownDataPoints, "Team1", slotToUpdate, at, remainingWork, doneWork);
			ChangeBurndownDataPoint(BurndownDataPoints, "Team2", slotToUpdate, at, remainingWork * 2.0, doneWork * 2.0);
#else
			var teams = workItems.AsParallel().AsUnordered().Select(x => x.Team).Distinct().ToArray();

			foreach (string team in teams)
			{
				string s = team;
				var teamWorkItems = workItems.Where(w => w.Team == s);

				double remainingWork = teamWorkItems.Sum(x => x.RemainingWork);
				double doneWork = teamWorkItems.Sum(x => x.DoneWork);

				ChangeBurndownDataPoint(DataPoints, team, slotToUpdate, at, remainingWork, doneWork);
			}
#endif
		}

		private void ProcessTeamMembersSprintBurndownForDay(DateTime at)
		{
			Debug.Assert(!Thread.CurrentThread.IsBackground);
			Debug.Assert(!this.processedBurndownDayDataEvent.WaitOne(0));

			lock (this.sprint.TeamMembers)
			{
				foreach (var teamMember in this.sprint.TeamMembers)
				{
					lock (this.workItemHistoryDictionary)
					{
						var workItems = this.workItemHistoryDictionary[at];

						ProcessIndividualSprintBurndownForDay(teamMember, workItems, at);
					}
				}
			}
		}

		private void ProcessTeamMembersSprintBurndownForDay(IEnumerable<IWorkItemAdapter> workItems, DateTime at, DateTime slotToUpdate)
		{
			Debug.Assert(!Thread.CurrentThread.IsBackground);
			Debug.Assert(!this.processedBurndownDayDataEvent.WaitOne(0));

			lock (this.sprint.TeamMembers)
			{
				foreach (var teamMember in this.sprint.TeamMembers)
				{
					ProcessIndividualSprintBurndownForDay(teamMember, workItems, at, slotToUpdate);
				}
			}
		}

		private static void ProcessIndividualSprintBurndownForDay(ITeamMember teamMember, IEnumerable<IWorkItemAdapter> workItems, DateTime at)
		{
			ProcessIndividualSprintBurndownForDay(teamMember, workItems, at, at);
		}

		private static void ProcessIndividualSprintBurndownForDay(ITeamMember teamMember, IEnumerable<IWorkItemAdapter> workItems, DateTime at,
		                                                          DateTime slotToUpdate)
		{
			double remainingWork = 0.0;
			double doneWork = 0.0;
#if FAKE_BURNDOWN_UPDATE
			remainingWork = (End - at).TotalHours;
			doneWork = (at - Start).TotalHours;
#else
			foreach (var workItemAdapter in workItems.Where(w => w.AssignedTo == teamMember.Information.Name))
			{
				remainingWork += workItemAdapter.RemainingWork;
				doneWork += workItemAdapter.DoneWork;
			}
#endif

			ChangeBurndownDataPoint(teamMember.BurndownDataPoints, teamMember.Information.Team, slotToUpdate, at, remainingWork, doneWork);
		}

		private static void ChangeBurndownDataPoint(BindingList<BurndownDataPoint> burndownDataPointList, string team, DateTime previousTime, DateTime at,
		                                            double remainingWork, double doneWork)
		{
			if (!double.IsNaN(remainingWork))
			{
				lock (burndownDataPointList)
				{
					var dataPoint = (previousTime == DateTime.MinValue ? null : burndownDataPointList.FirstOrDefault(x => x.Team == team && x.Time == previousTime));
					if (dataPoint != null)
					{
						dataPoint.Time = at;
						dataPoint.WorkDone = doneWork;
						dataPoint.WorkRemaining = remainingWork;
					}
					else
					{
						dataPoint = new BurndownDataPoint
						            	{
						            		Time = at,
						            		Team = team,
						            		WorkDone = doneWork,
						            		WorkRemaining = remainingWork
						            	};

						burndownDataPointList.Add(dataPoint);
					}
				}
			}
		}

		#endregion

		#region Implementation of ISprintBurndown

		public bool IsBuildingDataPoints
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				return this.isBuildingDataPoints;
			}
			private set
			{
				GuardDisposed();

				if (value != this.isBuildingDataPoints)
				{
					this.isBuildingDataPoints = value;
					AsyncRaisePropertyChanged("IsBuildingDataPoints");
				}
			}
		}

		public BindingList<BurndownDataPoint> DataPoints
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				return this.dataPointsList;
			}
		}

		public void BeginBuildBurndownData()
		{
			if (IsBuildingDataPoints)
			{
				return;
			}

			Action retrieveDailyWorkItems = RetrieveDailyWorkItems;

			retrieveDailyWorkItems.BeginInvoke(null, null);
		}

		#endregion

		#region Implementation of INotifyPropertyChanged

		public event PropertyChangedEventHandler PropertyChanged;

		private void RaisePropertyChanged(object state)
		{
			if (PropertyChanged != null)
			{
				string propertyName = (string) state;
				PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
			}
		}

		private void AsyncRaisePropertyChanged(string propertyName)
		{
			if (this.syncContext == null || !Thread.CurrentThread.IsBackground)
			{
				RaisePropertyChanged(propertyName);
			}
			else
			{
				this.syncContext.Post(RaisePropertyChanged, propertyName);
			}
		}

		#endregion

		#region Implementation of IDisposable

		public void Dispose()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}

		private void Dispose(bool disposing)
		{
			if (disposing && !this.disposed)
			{
				this.processedBurndownDayDataEvent.Close();

				this.disposed = true;
			}
		}

		#endregion
	}
}